package biolabs.core;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import jsr166y.ForkJoinPool;
import jsr166y.ForkJoinTask;

/**
 * Provides methods to setup and control the experiment.
 * Provides access to the hierarchy of bio-objects.
 *
 * <p>If there is a chance of simultaneous access to the
 * experiment or its bio-objects by another thread,
 * use {@link #executeUnderReadLock(Callable)} or
 * {@link #executeUnderWriteLock(Callable)} to execute your actions.
 * Keep this in mind when writing {@link ExperimentListener}s,
 * which can be run in parallel to other actions on the experiment.</p>
 *
 * @author Tomas Mikula
 */
public class Experiment {

	private final String name;
	private BioObject root = null;
	private long currentTime = 0;
	private final List<ExperimentListener> listeners = new LinkedList<ExperimentListener>();
	private final ForkJoinPool fjPool;
	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final BioContext context = new BioContext(){};

	/**
	 * Creates an empty experiment with the given name.
	 * @param name name to identify this experiment.
	 * @param fjPool Fork/Join pool used by this experiment.
	 * @param visConf visualization configuration used by the experiment.
	 * @throws NullPointerException if {@code name} or {@code logger}
	 * is {@code null}.
	 * @throws IllegalArgumentException if {@code name} is an empty string.
	 */
	public Experiment(String name, ForkJoinPool fjPool) {

		if(name == null)
			throw new NullPointerException("Name is null");
		if("".equals(name))
			throw new IllegalArgumentException("Empty name");
		if(fjPool == null)
			throw new NullPointerException("fjPool is null");

		this.name = name;
		this.fjPool = fjPool;
	}

	/**
	 * Runs this experiment for the specified number of simulation steps.
	 * @param steps non-negative number of simulation steps to perform.
	 * @throws ExecutionException if the simulation throws an exception.
	 * @throws IllegalStateException if the root object has not been specified
	 * (see {@link #setRoot(BioObject)}).
	 * @throws IllegalArgumentException if {@code steps} is negative.
	 */
	public void run(final long steps) throws ExecutionException {

		if(steps < 0)
			throw new IllegalArgumentException(
					"Negative number of steps: " + steps);
		if(steps == 0)
			return;

		executeUnderWriteLock(new Callable<Void>() {
			@Override
			public Void call() {
				if(root == null)
					throw new IllegalStateException("Root object is null");

				started();
				fjPool.invoke(ForkJoinTask.adapt(new Runnable() {
					@Override
					public void run() {
						root.proceed(currentTime, currentTime + steps, context);
					}
				}));
				currentTime += steps;
				return null;
			}
		});

		stopped();

	}

	public <T> T executeUnderReadLock(Callable<T> job) throws ExecutionException {
		lock.readLock().lock();
		try {
			return job.call();
		} catch(Throwable t) {
			throw new ExecutionException(t);
		} finally {
			lock.readLock().unlock();
		}
	}

	public <T> T executeUnderWriteLock(Callable<T> job) throws ExecutionException {
		lock.writeLock().lock();
		try {
			return job.call();
		} catch(Throwable t) {
			throw new ExecutionException(t);
		} finally {
			lock.writeLock().unlock();
		}
	}

	/**
	 * Returns the name of this experiment.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Sets the root object of this experiment. This method needs to be
	 * called exactly once for each experiment, prior to any call to
	 * {@link #run(long)}.
	 * @param root the root object of this experiment.
	 * @throws IllegalStateException if the root object has already been set.
	 * @throws NullPointerException if {@code root} is {@code null}.
	 */
	public void setRoot(BioObject root) {

		if(this.root != null)
			throw new IllegalStateException("Root has already been set");

		if(root == null)
			throw new NullPointerException("Root is null");

		this.root = root;

	}

	/**
	 * Returns the root object of this experiment,
	 * or {@code null} if this experiment is empty.
	 */
	public BioObject getRoot() {
		return root;
	}

	/**
	 * Returns the current time step, i.e. the number of steps
	 * passed from the start of the simulation.
	 */
	public long getStep() {
		return currentTime;
	}

	/**
	 * Returns a clock that emits a tick
	 * every time this experiment is stopped.
	 */
	public Clock asClock() {
		return new Clock() {
			@Override
			public void addClockListener(ClockListener l) {
				addListener(new ClockToExperimentListener(l));
			}
			@Override
			public void removeClockListener(ClockListener l) {
				removeListener(new ClockToExperimentListener(l));
			}
		};
	}

	/**
	 * Returns a clock that emits a tick every time this experiment is
	 * stopped. The tick is executed under this experiment's read lock.
	 */
	public Clock asReadLockedClock() {
		return new Clock() {
			@Override
			public void addClockListener(ClockListener l) {
				addListener(new ClockToReadLockedExperimentListener(l));
			}
			@Override
			public void removeClockListener(ClockListener l) {
				removeListener(new ClockToReadLockedExperimentListener(l));
			}
		};
	}

	/**
	 * Returns a clock that emits a tick every time this experiment is
	 * stopped. The tick is executed under this experiment's write lock.
	 */
	public Clock asWriteLockedClock() {
		return new Clock() {
			@Override
			public void addClockListener(ClockListener l) {
				addListener(new ClockToWriteLockedExperimentListener(l));
			}
			@Override
			public void removeClockListener(ClockListener l) {
				removeListener(new ClockToWriteLockedExperimentListener(l));
			}
		};
	}

	/**
	 * Closes this experiment, releasing occupied resources.
	 * Implementations may want to override this method, but should
	 * not forget to call this inherited method from the overriding
	 * method's body.
	 */
	public synchronized void close() {
		if(root != null)
			root.close();

		// allow the members to be garbage collected,
		// even if the reference to this experiment is kept
		root = null;

		closed();
	}

	@Override
	public String toString() {
		return "experiment '" + name + '\'';
	}

	/**
	 * Returns an array of currently registered listeners.
	 * Used when calling the listeners, so that the listeners
	 * can add or remove other listeners (or self) without
	 * causing a concurrent modification of the listeners list.
	 */
	private ExperimentListener[] _listeners() {
		return listeners.toArray(new ExperimentListener[listeners.size()]);
	}

	/**
	 * Notifies the listeners that this experiment started (resumes).
	 */
	private void started() {
		for(ExperimentListener l: _listeners())
			l.experimentStarted(this);
	}

	/**
	 * Notifies the listeners that this experiment was stopped (paused).
	 */
	private void stopped() {
		for(ExperimentListener l: _listeners())
			l.experimentStopped(this);
	}

	/**
	 * Notifies the listeners that this experiment was closed.
	 */
	private void closed() {
		for(ExperimentListener l: _listeners())
			l.experimentClosed(this);
	}

	public void addListener(ExperimentListener l) {
		listeners.add(l);
	}

	public void removeListener(ExperimentListener l) {
		listeners.remove(l);
	}

}